/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All Rights Reserved.
 * Description: dump page table
 * Author: cheng chao
 * Create: 2021-6-9
 */

#include <asm/page.h>
#include <asm/pgtable.h>
#include <linux/mm.h>
#include <linux/hugetlb.h>

#define ROW_DUMP_BYTES		16
#define ROW_BASE_ADDR_LEN	8
#define ROW_LEN			(ROW_DUMP_BYTES * 3 + ROW_BASE_ADDR_LEN + 1)

static unsigned char dump_page_data[SZ_4K] = {0};

#if defined(CONFIG_X86)
static int bad_address(void *p)
{
	unsigned long dummy;

	return get_kernel_nofault(dummy, (unsigned long *)p);
}
#endif

#if defined(CONFIG_ARM) || defined(CONFIG_ARM64)
static void get_current_mm(struct mm_struct **p_mm,
	unsigned long addr)
{
	if (addr >= MODULES_VADDR) /* kernel addr use kernel page table */
		*p_mm = &init_mm;
	else
		*p_mm = current->mm; /* user addr use user page table */

	if (*p_mm == NULL) /* kernel thread, current->mm is NULL */
		*p_mm = &init_mm;
}
#endif

static bool is_data_zero_row(unsigned int row_num, unsigned int dump_size)
{
	unsigned int i;
	unsigned int row_offset_base = row_num * ROW_DUMP_BYTES;
	unsigned int total_row_num = dump_size / ROW_DUMP_BYTES;

	if (row_num >= total_row_num)
		return false;

	for (i = 0; i < ROW_DUMP_BYTES; i++) {
		if (dump_page_data[row_offset_base + i] != 0)
			return false;
	}
	return true;
}

static unsigned int get_zero_region_end_row(unsigned int zero_region_start_row,
	unsigned int dump_size)
{
	unsigned int row_num = zero_region_start_row;
	unsigned int total_row_num = dump_size / ROW_DUMP_BYTES;

	while (row_num < total_row_num) {
		row_num++;
		if (!is_data_zero_row(row_num, dump_size))
			break;
	}

	return row_num - 1;
}

static void dump_one_row_data(unsigned int current_row)
{
	int i;
	unsigned int row_base = current_row * ROW_DUMP_BYTES;
	unsigned char str_buf[ROW_LEN] = {0};

	sprintf(str_buf, "%.8x", row_base);
	for (i = 0; i < ROW_DUMP_BYTES; i++)
		sprintf(str_buf + ROW_BASE_ADDR_LEN + i * 3, " %.2x",
			(unsigned int)dump_page_data[row_base + i]);
	str_buf[ROW_DUMP_BYTES * 3 + ROW_BASE_ADDR_LEN] = '\0';
	pr_info("%s\n", str_buf);
}

static void pagetable_hexdump(unsigned char *buf, unsigned int dump_size)
{
	unsigned int current_row = 0;
	unsigned int zero_region_start_row;
	unsigned int zero_region_end_row;
	unsigned int total_row_num = dump_size / ROW_DUMP_BYTES;

	while (current_row < total_row_num) {
		if (is_data_zero_row(current_row, dump_size)) {
			zero_region_start_row = current_row;
			zero_region_end_row =
				get_zero_region_end_row(zero_region_start_row,
					dump_size);
			pr_info("%.8x ----> %.8x: 00000000\n",
				zero_region_start_row * ROW_DUMP_BYTES,
				(zero_region_end_row + 1) * ROW_DUMP_BYTES - 1);
			current_row += (zero_region_end_row
				- zero_region_start_row + 1);
		} else {
			dump_one_row_data(current_row);
			current_row++;
		}
	}
}

static void pagetable_dump_item(unsigned long base_addr,
		unsigned long index_addr, unsigned int pagetable_size)
{
	unsigned int dump_size;
	unsigned long dump_addr;
	unsigned long offset;

	if (pagetable_size > SZ_4K) {
		/*
		 In the starting 1K area, print 2K content from the starting address;
		 In the end 1K area, print the last 2K content;
		 In the middle area, there are 2K contents before and after printing
		*/
		dump_size = SZ_2K;

		offset = index_addr - base_addr;
		if (offset <= SZ_1K)
			dump_addr = base_addr;
		else if ((offset + SZ_1K) >= pagetable_size)
			dump_addr = base_addr + pagetable_size - SZ_2K;
		else
			dump_addr = ALIGN_DOWN(index_addr, SZ_1K);
	} else {
		dump_size = pagetable_size;
		dump_addr = base_addr;
	}

	pr_info(" base addr = %lx\n", dump_addr);
	pr_info("index addr = %lx\n", index_addr);
	memcpy(dump_page_data, (void *)dump_addr, dump_size);
	pagetable_hexdump(dump_page_data, dump_size);
	pr_info("\n");
}

static pgd_t *get_pgd(unsigned long addr)
{
	pgd_t *pgd = NULL;
#if defined(CONFIG_X86)
	pgd_t *base = NULL;

	base = __va(read_cr3_pa());
	pgd = base + pgd_index(addr);
#elif defined(CONFIG_ARM) || defined(CONFIG_ARM64)
	struct mm_struct *mm = NULL;

	get_current_mm(&mm, addr);
	pgd = pgd_offset(mm, addr);
#endif
	return pgd;
}

static bool check_param_before_dump_pgd(pgd_t *pgd)
{
	pr_info("*pgd = %llx\n", (unsigned long long)pgd_val(*pgd));
#if defined(CONFIG_X86)
	if ((bad_address(pgd)) || (!pgd_present(*pgd)))
		return false;
#elif defined(CONFIG_ARM) || defined(CONFIG_ARM64)
	if (pgd_none(*pgd) || pgd_bad(*pgd))
		return false;
#endif
	return true;
}

static bool check_param_before_dump_p4d(p4d_t *p4d)
{
#if defined(CONFIG_X86) /* only x86 use p4d */
	pr_info("*p4d = %llx\n", (unsigned long long)p4d_val(*p4d));
	if (!p4d_present(*p4d) || p4d_large(*p4d) || bad_address(p4d))
		return false;
#elif defined(CONFIG_ARM) || defined(CONFIG_ARM64)
	/* arm64, pgd_none/pgd_bad is useless, need check wheter p4d is effective */
	if (p4d_none(*p4d) || p4d_bad(*p4d))
		return false;
#endif

	return true;
}

static bool check_param_before_dump_pud(pud_t *pud)
{
	pr_info("*pud = %llx\n", (unsigned long long)pud_val(*pud));
#if defined(CONFIG_ARM) || defined(CONFIG_ARM64)
	if (pud_none(*pud) || pud_bad(*pud))
		return false;
#elif defined(CONFIG_X86)
	if ((bad_address(pud)) || (!pud_present(*pud) || pud_large(*pud)))
		return false;
#endif
	return true;
}

static bool check_param_before_dump_pmd(pmd_t *pmd)
{
	pr_info("*pmd = %llx\n", (unsigned long long)pmd_val(*pmd));
#if defined(CONFIG_X86)
	if ((bad_address(pmd)) || (!pmd_present(*pmd) || pmd_large(*pmd)))
		return false;
#elif defined(CONFIG_ARM) || defined(CONFIG_ARM64)
	if ((pmd_none(*pmd)) || (pmd_bad(*pmd)))
		return false;
#endif

#if defined(CONFIG_ARM)
	/* We must not map this if we have highmem enabled */
	if (PageHighMem(pfn_to_page(pmd_val(*pmd) >> PAGE_SHIFT)))
		return false;
#endif
	return true;
}

static bool check_param_before_dump_pte(pte_t *pte)
{
	pr_info("*pte = %llx\n", (unsigned long long)pte_val(*pte));
#if defined(CONFIG_X86)
	if (bad_address(pte))
		return false;
#endif
	return true;
}

static void dump_pgd(pgd_t *pgd, unsigned long addr)
{
	unsigned long base;
	unsigned long index;
	unsigned int pagetable_size;

#if defined(CONFIG_ARM64) || defined(CONFIG_ARM)
	struct mm_struct *mm = NULL;

	get_current_mm(&mm, addr);
	base = (unsigned long)(uintptr_t)mm->pgd;
#elif defined(CONFIG_X86)
	base = (unsigned long)(uintptr_t)__va(read_cr3_pa());
#endif
	index = (unsigned long)(uintptr_t)pgd;

	pagetable_size = (unsigned int)(PTRS_PER_PGD * sizeof(pgd_t));
	pagetable_dump_item(base, index, pagetable_size);
	pr_info("\n");
}

static void dump_p4d(pgd_t *pgd, p4d_t *p4d)
{
	unsigned long base;
	unsigned long index;
	unsigned int pagetable_size;

#ifndef CONFIG_X86
	return;
#endif

	if ((unsigned long)(uintptr_t)pgd == (unsigned long)(uintptr_t)p4d)
		return;

	index = (unsigned long)(uintptr_t)p4d;
	base = (unsigned long)pgd_page_vaddr(*pgd);
	pagetable_size = (unsigned int)(PTRS_PER_P4D * sizeof(p4d_t));
	pagetable_dump_item(base, index, pagetable_size);
	pr_info("\n");
}

static void dump_pud(p4d_t *p4d, pud_t *pud)
{
	unsigned long base;
	unsigned long index;
	unsigned int pagetable_size;

	if ((unsigned long)(uintptr_t)p4d == (unsigned long)(uintptr_t)pud)
		return;

	index = (unsigned long)(uintptr_t)pud;

	base = (unsigned long)(uintptr_t)p4d_pgtable(*p4d);
	pagetable_size = (unsigned int)(PTRS_PER_PUD * sizeof(pud_t));
	pagetable_dump_item(base, index, pagetable_size);
	pr_info("\n");
}

static void dump_pmd(pud_t *pud, pmd_t *pmd)
{
	unsigned long base = 0;
	unsigned long index;
	unsigned int pagetable_size;

	if ((unsigned long)(uintptr_t)pud == (unsigned long)(uintptr_t)pmd)
		return;

	index = (unsigned long)(uintptr_t)pmd;
	base = (unsigned long)(uintptr_t)pud_pgtable(*pud);
	pagetable_size = (unsigned int)(PTRS_PER_PMD * sizeof(pmd_t));
	pagetable_dump_item(base, index, pagetable_size);
	pr_info("\n");
}

static void dump_pte(pgd_t *pgd, pmd_t *pmd,
		pte_t *pte)
{
	unsigned long base;
	unsigned long index;
	unsigned int pagetable_size;

	index = (unsigned long)(uintptr_t)pte;

#if defined(CONFIG_ARM)
	if (PageHighMem(pfn_to_page(pmd_val(*pmd) >> PAGE_SHIFT)))
		return;
#ifndef CONFIG_ARM_LPAE
	pr_info("*ppte = %08llx\n",
		(unsigned long long)pte_val(pte[PTE_HWTABLE_PTRS]));
#endif
#endif

	base = (unsigned long)(uintptr_t)(pte_t *)pmd_page_vaddr(*pmd);
	pagetable_size = (unsigned int)(PTRS_PER_PTE * sizeof(pte_t));
	pagetable_dump_item(base, index, pagetable_size);
	pr_info("\n");
}

void rtos_dump_pagetable(unsigned long addr)
{
	pgd_t *pgd = NULL;
	p4d_t *p4d = NULL;
	pud_t *pud = NULL;
	pmd_t *pmd = NULL;
	pte_t *pte = NULL;

	do {
		pgd = get_pgd(addr);
		if (!check_param_before_dump_pgd(pgd))
			break;
		dump_pgd(pgd, addr);

		p4d = p4d_offset(pgd, addr);
		if (!check_param_before_dump_p4d(p4d))
			break;
		dump_p4d(pgd, p4d);

		pud = pud_offset(p4d, addr);
		if (!check_param_before_dump_pud(pud))
			break;
		dump_pud(p4d, pud);

		pmd = pmd_offset(pud, addr);
		if (!check_param_before_dump_pmd(pmd))
			break;
		dump_pmd(pud, pmd);

		pte = pte_offset_kernel(pmd, addr);
		if (!check_param_before_dump_pte(pte))
			break;
		dump_pte(pgd, pmd, pte);
	} while (0);

	pr_info("\n");
}
